/*
 * CalibratedMap.java
 *
 * Created on 27 avril 2007, 16:44
 *
 */

package courselog.maputils;

import courselog.CalibrateForm;
import courselog.CourseLogAppForm;
import courselog.NavigateForm;
import courselog.gpsutils.Converters;
import courselog.gpsutils.GeoDirection;
import courselog.gpsutils.GeoPoint;
import ewe.fx.IImage;
import ewe.fx.Image;
import ewe.fx.ImageData;
import ewe.fx.ImageTool;
import ewe.fx.PNGEncoder;
import ewe.fx.Point;
import ewe.fx.Rect;
import ewe.io.BufferedOutputStream;
import ewe.io.BufferedReader;
import ewe.io.File;
import ewe.io.FileNotFoundException;
import ewe.io.FileOutputStream;
import ewe.io.FileReader;
import ewe.io.IOException;
import ewe.sys.Convert;
import ewe.sys.mThread;
import ewe.ui.MessageBox;
import ewe.ui.ScrollServer;
import ewe.util.Vector;

/**
 *
 * @author rigal
 */
public class CalibratedMap implements GeoGraphable {
    
    /** Extension associated to files containing reference points.
     */
    public final static String REFEXTENSION=".mpr";
    
    /** Extension associated to files containing reference points.
     */
    public final static int UNINITIALISEDSIZE=-1;
    
    /** Markup for coordinates in KML path file.
     */
    public final static String COORDINATESMARKUP="<coordinates>";
    
    /** Vector of reference points associated to calibration.
     */
    public Vector vRefPoints=new Vector(2);
    
    /** Index of first selected reference point.
     */
    public int idxRP1;
    
    /** Index of second selected reference point.
     */
    public int idxRP2;
    
    /** Bitmap representation of the map that is used.
     */
    protected Image imageMap=null;
    
    /** Image data representation of the map that is used.
     */
    protected ImageData imageDataMap=null;
    
    /** Image width */
    public int imgW=UNINITIALISEDSIZE;
    
    /** Image height */
    public int imgH=UNINITIALISEDSIZE;
    
    /** File name of the image file.
     */
    public String stMapFileName;
    
    /** Map Center position
     */
    public GeoPoint gpCenter=new GeoPoint();
    
    /** Center north direction
     */
    public GeoDirection vpNorth=new GeoDirection();
    
    /** Center east direction
     */
    public GeoDirection vpEast= new GeoDirection();
    
    /** Map scaling factor.
     */
    public double dScPixelOnMeters;
    
    /** Map upper left corner latitude.
     */
    public double maxLat;
    
    /** Map upper left corner longitude.
     */
    public double minLon;
    
    /** Map lower right corner latitude.
     */
    public double minLat;
    
    /** Map lower right corner longitude.
     */
    public double maxLon;
    
    // CONSTRUCTORS
    
    /** Creates a new instance from the map name */
    public CalibratedMap(String imageFileName) {
        this.stMapFileName=imageFileName;
        initCalibratedMap();
    }
    
    // ACCESSORS
    
    public Image getImageMap() {
        if(imageMap==null) initCalibratedMapImage();
        return imageMap;
    }
    
    public void setImageMap(Image imageMap) {
        if(this.imageMap!=null) this.imageMap.free();
        initCalibratedMapImage(imageMap);
    }
    
    public ImageData getImageDataMap() {
        if(imageDataMap==null) initCalibratedMapImage();
        return imageDataMap;
    }
    
    // CLASS METHODS
    
    /** Initialise current instance from the bitmap file name */
    public void initCalibratedMapImage() {
        String fullMapName=stMapFileName;
        initCalibratedMapImage(new Image(this.stMapFileName));
//        new MessageBox("initCalibratedMapImage",fullMapName,MessageBox.OKB).execute();
    }
    
    /** Initialise current reference points from a kml path file. */
    public void initReferenceLocationsFromKML(File fKML) {
        
        try {
            if(!fKML.canRead()) return;
            
            FileReader fKMLRead = new FileReader(fKML);
            
            String ln;
            double val=0.0d;
            boolean hasFoundCoordinates=false;
            while((ln=fKMLRead.readLine())!=null) {
                if(hasFoundCoordinates) {
                    int idx=0;
                    int idxEnd=0;
                    int idxRefPoint=0;
                    byte[] buf=ln.getBytes();
                    final String parseKMLPoint="VVV";
                    double[] dbV=new double[3];
                    while((idx<ln.length())&&(idxRefPoint<vRefPoints.size())) {
                        MapReferencePoint mpR=(MapReferencePoint) vRefPoints.elementAt(idxRefPoint);
                        
                        idxEnd=ln.indexOf(' ',idx);
                        Converters.parseCSVSentence(parseKMLPoint,buf,idx,idxEnd-1,dbV,null,null);
                        mpR.lon=dbV[0];
                        mpR.lat=dbV[1];
                        idx=idxEnd+1;
                        mpR.computeCartesian();
                        idxRefPoint++;
                    }
                    break;
                }else{
                    hasFoundCoordinates=ln.endsWith(COORDINATESMARKUP);
                }
                
            }
            fKMLRead.close();
        } catch (FileNotFoundException ex) {
            CourseLogAppForm.dbgL.addExceptionToLog(ex);
            return;
        } catch (IOException ex) {
            CourseLogAppForm.dbgL.addExceptionToLog(ex);
            return;
        } catch (NumberFormatException ex) {
            CourseLogAppForm.dbgL.addExceptionToLog(ex);
            return;
        }
        
    }
    
    /** Initialise current instance from the bitmap image */
    public void initCalibratedMapImage(Image img) {
        if(this.imageMap!=null) {
            this.imageMap.free();
        }
        this.imageMap= img;
        this.imgW=this.imageMap.getWidth();
        this.imgH=this.imageMap.getHeight();
        this.imageDataMap=ImageTool.toImageData(this.imageMap);
//        mThread msg=new mThread(new Runnable() {
//            public void run() {
//                CourseLogAppForm.dbgL.addToLog("initCalibratedMapImage: width "+imgW+" height "+imgH+"\n");
//            }
//        });
//        msg.run();
    }
    
    /** Free image data */
    public void free() {
        if(this.imageMap==null) return;
        this.imageMap.free();
        this.imageMap=null;
        this.imageDataMap=null;
    }
    
    /** Initialises a new instance from the bitmap name */
    public void initCalibratedMap() {
        if(imageMap!=null) {
            imageMap.free();
            imageMap=null;
            imageDataMap=null;
        }
        String referencesFileName=getReferenceFileName();
//        CourseLogAppForm.dbgL.addToLog("\nNom :"+referencesFileName);
//        new MessageBox("nom",referencesFileName,MessageBox.OKB).execute();
        File fRef= new File(referencesFileName);
        
        // Get the map directory name
        String dirName=fRef.getParent();
        
        vRefPoints.setSize(0);
        
        if(fRef.canRead()) {
            try {
                BufferedReader brRef= new BufferedReader(new ewe.io.FileReader(fRef));
                String ln;
                // Read bitmap file name (depending on extension, it may
                // Not be strictly unique to find.
                if((ln=brRef.readLine())==null) return;
                this.stMapFileName=dirName+"/"+ln.trim();
                /*
                CourseLogAppForm.dbgL.addToLog("\nImage File :"+stMapFileName);*/
//                new MessageBox("image file",stMapFileName,MessageBox.OKB).execute();
                if((ln=brRef.readLine())==null) return;
//                String msg = ln+"\n";
                int viSelRef[]= new int[3];
                Converters.parseCSVSentence("II",ln.getBytes(),0,ln.length()-1,
                        null,viSelRef,null);
                imgW=viSelRef[0];
                imgH=viSelRef[1];
                if((ln=brRef.readLine())==null) return;
//                msg = msg +ln+"\n";
                Converters.parseCSVSentence("III",ln.getBytes(),0,ln.length()-1,
                        null,viSelRef,null);
//                new MessageBox(stMapFileName,msg,MessageBox.OKB).execute();
                int nbPt=viSelRef[0];
                idxRP1=viSelRef[1];
                idxRP2=viSelRef[2];
                
                for(int idxP=0; idxP<nbPt; idxP++) {
                    if((ln=brRef.readLine())!=null) {
                        MapReferencePoint mPt = new MapReferencePoint();
                        if(mPt.readFromCsvLine(ln)) {
                            vRefPoints.add(mPt);
                        }
                    }
                }
                brRef.close();
            } catch (ewe.io.IOException ex) {
                CourseLogAppForm.dbgL.addExceptionToLog(ex);
            }
            findOptimalRefPoint();
            computeTransformValues();
            GeoPoint gp=null;
            gp=getPointFromMap(0,0,gp);
            maxLat=gp.getLat();
            minLon=gp.getLon();
            gp=getPointFromMap(imgW-1,imgH-1,gp);
            minLat=gp.getLat();
            maxLon=gp.getLon();
        }else{
            idxRP1=-1;
            idxRP2=-1;
        }
    }
    
    /** Save the reference points into a CSV file. */
    public boolean saveReferenceFile(File fRef) {
        if((fRef.canWrite())||(!fRef.exists())) {
            try {
                BufferedOutputStream bwRef= new BufferedOutputStream(
                        new ewe.io.FileOutputStream(fRef));
                byte[] buf=new byte[256];
                File fImg= new File(this.stMapFileName);
                bwRef.write((fImg.getFileExt()+'\n').getBytes());
                if(imgW==UNINITIALISEDSIZE) {
                    initCalibratedMapImage();
                }
                String stSize=imgW+","+imgH+"\n";
                bwRef.write(stSize.getBytes());
                int nbPt=vRefPoints.getCount();
                String stHeader=nbPt+","+idxRP1+","+idxRP2+"\n";
                bwRef.write(stHeader.getBytes());
                
                int idxB=0;
                for(int idxP=0; idxP<nbPt; idxP++) {
                    MapReferencePoint mPt = (MapReferencePoint) vRefPoints.elementAt(idxP);
                    idxB=mPt.formatInBuffer(buf);
                    bwRef.write(buf,0,idxB);
                }
                bwRef.flush();
                bwRef.close();
                return true;
            } catch (ewe.io.IOException ex) {
                CourseLogAppForm.dbgL.addExceptionToLog(ex);
            }
        }
        return false;
    }
    
    /** Save a part of the bitmap to an image file and the corresponding reference file.
     */
    public boolean saveSubMap(Rect rArea, String imageFileName, CalibratedMap tmp) {
        if(imageDataMap==null) {
            initCalibratedMapImage();
        }
        if(tmp==null) {
            tmp=new CalibratedMap(imageFileName);
            Image subMap=ImageTool.createCompatibleImage(imageDataMap,
                    rArea.width,rArea.height);
            tmp.setImageMap(subMap);
        }
        
        File fW=new File(imageFileName);
        if((!fW.canWrite())&&(fW.exists())) return false;
        try {
            FileOutputStream fOS=new FileOutputStream(fW);
            if((tmp.imageMap.getWidth()!=rArea.width)||
                    (tmp.imageMap.getHeight()!=rArea.height)) {
                tmp.free();
                tmp.setImageMap(ImageTool.createCompatibleImage(imageDataMap,
                        rArea.width,rArea.height));
            }
            ImageTool.scaleSection(imageDataMap,
                    imgW, imgH,tmp.imageDataMap,
                    rArea.x,rArea.y, ImageTool.SCALE_KEEP_ASPECT_RATIO);
            PNGEncoder pngE=new PNGEncoder();
            pngE.writeImage(fOS.toWritableStream(),(Image) tmp.imageMap);
            fOS.flush();
            fOS.close();
            
            // Reference file
            MapReferencePoint r1;
            MapReferencePoint r2;
            
            if(tmp.vRefPoints.size()<2) {
                r1=new MapReferencePoint();
                r2=new MapReferencePoint();
            }else {
                r1=(MapReferencePoint) tmp.vRefPoints.elementAt(0);
                r2=(MapReferencePoint) tmp.vRefPoints.elementAt(1);
            }
            r1.iX=0;
            r1.iY=0;
            r1=(MapReferencePoint) this.getPointFromMap(rArea.x,rArea.y,r1);
            
            // Re-use coordinates of r2 to store position in new map
            r2.iX=rArea.width-1;
            r2.iY=rArea.height-1;
            r2=(MapReferencePoint) this.getPointFromMap(rArea.x+rArea.width-1,
                    rArea.y+rArea.height-1,r2);
            // Set reference points vector
            tmp.stMapFileName=imageFileName;
            tmp.vRefPoints.setSize(0);
            tmp.vRefPoints.add(r1);
            tmp.vRefPoints.add(r2);
            tmp.findOptimalRefPoint();
//            tmp.computeTransformValues();
            fW= new File(tmp.getReferenceFileName());
            if((!fW.exists())||(fW.canWrite())) {
                tmp.saveReferenceFile(fW);
            }else{
                // Impossible to write reference file
                return false;
            }
        } catch (IOException ex) {
            CourseLogAppForm.dbgL.addExceptionToLog(ex);
            return false;
        }
        return true;
    }
    
    /**
     * Select closest reference point from a point position on the map.
     *
     */
    public MapReferencePoint findClosestReferencepoint(Point p, double maxD) {
        double distMin=maxD*maxD;
        double dV;
        double dT;
        MapReferencePoint mr=null;
        for(int idxP=vRefPoints.size()-1; idxP>=0; idxP--) {
            MapReferencePoint mC= (MapReferencePoint) vRefPoints.elementAt(idxP);
            dV=p.x-mC.iX;
            dT=dV*dV;
            dV=p.y-mC.iY;
            dT += dV*dV;
            if(distMin>=dT) {
                distMin=dT;
                mr=mC;
            }        }
        return mr;
    }
    
    /**
     * Compute optimal reference points to use.
     * Test all points and verify that the others are close from their
     * given position.
     */
    public void findOptimalRefPoint() {
        if(vRefPoints.size()<2) {
            idxRP1=-1;
            idxRP2=-1;
            return;
        }
        
        MapReferencePoint pT= new MapReferencePoint();
        Point ptT=new Point();
        GeoDirection gdT=new GeoDirection();
        
        double dMinDist=1E10d;
        for(int idxP1=vRefPoints.size()-1; idxP1>=0; idxP1--) {
            MapReferencePoint p1=(MapReferencePoint) vRefPoints.elementAt(idxP1);
            for(int idxP2=vRefPoints.size()-1; idxP2>=0; idxP2--) {
                if(idxP2==idxP1) continue; // Skip same points 1&2
                MapReferencePoint p2=(MapReferencePoint) vRefPoints.elementAt(idxP2);
                computeTransformValues(p1,p2);
                double dCurDist=0.0d;
                for(int idxP3=vRefPoints.size()-1; idxP3>=0; idxP3--) {
                    MapReferencePoint pTC=(MapReferencePoint) vRefPoints.elementAt(idxP3);
                    pT.copyFrom(pTC);
                    ptT=getMapPointFromPosition(pT,gdT,ptT);
                    // Compute sort of a distance: sum of square errors
                    int iV=ptT.x-pT.iX;
                    dCurDist += iV*iV;
                    iV=ptT.y-pT.iY;
                    dCurDist += iV*iV;
                }
                if(dMinDist>dCurDist) {
                    // New minimum distance point
                    idxRP1=idxP1;
                    idxRP2=idxP2;
                    dMinDist=dCurDist;
                }
            }
        }
    }
    
    /**
     * Compute transformation values from the 2 identified reference points.
     */
    public void computeTransformValues() {
        MapReferencePoint pt1=(MapReferencePoint) vRefPoints.elementAt(idxRP1);
        MapReferencePoint pt2=(MapReferencePoint) vRefPoints.elementAt(idxRP2);
        computeTransformValues(pt1,pt2);
    }
    
    /**
     * Compute transformation values from 2 reference points.
     */
    public void computeTransformValues(MapReferencePoint pt1, MapReferencePoint pt2) {
        double dX= pt2.iX-pt1.iX;
        double dY= pt2.iY-pt1.iY;
        double dPixel= Math.sqrt(dX*dX+dY*dY);
        
        // Compute scale
        dScPixelOnMeters=dPixel/pt1.distance(pt2);
        // Local vectors of pt1
        GeoDirection.vectorsNorthEast(pt1,vpNorth,vpEast);
        // Move to Map center (width/2,heigth/2)
        gpCenter=pt1.moveBy(true,vpEast,(imgW/2-pt1.iX)/dScPixelOnMeters,
                vpNorth,(pt1.iY-imgH/2)/dScPixelOnMeters,
                gpCenter);
        // recompute north and east from map center
        GeoDirection.vectorsNorthEast(gpCenter,vpNorth,vpEast);
    }
    
    /**
     * Compute lat and long from position on map.
     */
    public GeoPoint getPointFromMap(int xx, int yy, GeoPoint dest) {
        // Position relative to image center
        xx -= imgW/2;
        yy = imgH/2-yy;
        // Move center by xx and yy
        dest=gpCenter.moveBy(true,vpEast,xx/dScPixelOnMeters,
                vpNorth,yy/dScPixelOnMeters,dest);
        
        return dest;
    }
    
    /**
     * Compute X and Y on map from geo position.
     * @param pos geo position to transpose.
     * @param tmp GeoDirection reference to compute intermediate direction.
     * If <I><B>null</B></I> a new object is allocated.
     * @param pt point on bitmap image to compute.
     * If <I><B>null</B></I> a new Point object is allocated.
     * @return Computed point. Should be <I>pt</I> or a new Point object.
     */
    public Point getMapPointFromPosition(GeoPoint pos, GeoDirection tmp,
            Point pt) {
        // Allocate point if needed
        if(pt==null) {
            pt=new Point();
        }
        
        // Direction
        tmp=this.gpCenter.directionTo(pos,tmp);
        // Scalar product with east vector
        double dX=tmp.x*this.vpEast.x+tmp.y*vpEast.y+tmp.z*vpEast.z;
        double dY=tmp.x*this.vpNorth.x+tmp.y*vpNorth.y+tmp.z*vpNorth.z;
        
        pt.x = (int) (dX*dScPixelOnMeters+imgW/2+0.5d);
        pt.y = (int) (imgH/2-dY*dScPixelOnMeters+0.5d);
        
        return pt;
    }
    
    /**
     * Get the reference file name formed from the Map image file name with
     * the REFEXTENSION extension.
     */
    public String getReferenceFileName() {
        int idxExt=stMapFileName.lastIndexOf('.');
        return stMapFileName.substring(0,idxExt)+REFEXTENSION;
    }
    
}